/*
 * httprequest.cpp
 *
 *  Created on: Aug 28, 2011
 *      Author: anonovermind <anonovermind@gmail.com>
 */

#include "../include/httprequest.h"
#include <boost/asio/streambuf.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>
#include <cstdlib>
#include "exceptions.h"

namespace Wishmaster2 {
namespace Network {

using namespace boost::algorithm;

HttpRequest::HttpRequest(boost::asio::io_service& service,
		HttpCachingResolver* resolver,
		int id,
		const Url& url,
		const HttpNetworkContextPtr& context) : m_resolver(resolver), m_id(id), m_url(url),
		m_statusCode(0),
		m_socket(service),
		m_request(nullptr),
		m_response(nullptr),
		m_content(new std::vector<char>()),
		m_expectedLength(0),
		m_readLength(0),
		m_method("GET"),
		m_chunked(false),
		m_context(context)
{

}

HttpRequest::~HttpRequest()
{
	if(m_response)
		delete m_response;
	if(m_request)
		delete m_request;
}

int HttpRequest::id() const
{
	return m_id;
}

boost::signals2::connection HttpRequest::connectSignalFinished(const finished_signal::slot_type& finished)
{
	return m_finished.connect(finished);
}

boost::signals2::connection HttpRequest::connectSignalProgress(const progress_signal::slot_type& progress)
{
	return m_progress.connect(progress);
}

boost::signals2::connection HttpRequest::connectSignalError(const error_signal::slot_type& error)
{
	return m_error.connect(error);
}

void HttpRequest::perform()
{
	m_resolver->resolve(m_url.host(), boost::bind(&HttpRequest::handle_resolve, this,
			boost::asio::placeholders::error, boost::asio::placeholders::iterator));
}

void HttpRequest::cancel()
{
	m_socket.cancel();
}

VectorPtr HttpRequest::content()
{
	return m_content;
}

std::string HttpRequest::contentType() const
{
	return header("content-type");
}

Url HttpRequest::url() const
{
	return m_url;
}

int HttpRequest::statusCode() const
{
	return m_statusCode;
}

void HttpRequest::setPostBoundary(const std::string& boundary)
{
	m_boundary = boundary;
}

std::string HttpRequest::header(const std::string& name) const
{
	std::string lowered = name;
	to_lower(lowered);
	auto it = m_headers.find(lowered);
	if(it == m_headers.end())
		return std::string();
	return (*it).second;
}

void HttpRequest::setParameter(const std::string& paramName, const std::string& paramValue)
{
	if(paramName == "http:referer")
	{
		m_referer = paramValue;
	}
	else if(paramName == "http:method")
	{
		std::string raised = paramValue;
		boost::to_upper(raised);
		m_method = raised;
	}
	else
	{
		m_params[paramName] = paramValue;
	}
}

void HttpRequest::addBinaryData(const std::string& paramName, const std::string& dataName, const std::vector<char>& data)
{
	m_files[paramName] = FileDescriptor {dataName, data};
}

void HttpRequest::handle_resolve(const boost::system::error_code& error,
			boost::asio::ip::tcp::resolver::iterator endpoint_iterator)
{
	if(!error)
	{
		boost::asio::async_connect(m_socket, endpoint_iterator,
				boost::bind(&HttpRequest::handle_connect, this, boost::asio::placeholders::error));
	}
	else
	{
		m_error(id(), error.message());
	}
}

void HttpRequest::handle_connect(const boost::system::error_code& error)
{
	if(!error)
	{
		if((m_method != "GET") && (m_method != "POST"))
		{
			m_error(id(), std::string("HttpRequest: Invalid method: ") + m_method);
			return;
		}
		m_request = createRequestStreamBuf();
		m_response = createResponseStreamBuf();
		std::ostream request_stream(m_request);
		request_stream << m_method + " " + m_url.path() + " HTTP/1.1\r\n";
		request_stream << "Host: " + m_url.host() << "\r\n";
		if(!m_referer.empty())
		{
			request_stream << "Referer: " << m_referer << "\r\n";
		}
		std::string cookiestring = m_context->cookieStringForDomain(m_url.host());
		if(!cookiestring.empty())
		{
			request_stream << "Cookie: " << cookiestring << "\r\n";
		}
		std::stringstream data_stream;
		if(m_method == "POST")
		{
			if(m_boundary.empty())
			{
				char boundaryBuffer[68];
				for(int i = 0; i < 64; i++)
					boundaryBuffer[i] = randomChar();
				boundaryBuffer[64] = 0;
				m_boundary = "--------";
				m_boundary += std::string(boundaryBuffer);
			}
			request_stream << "Content-Type: multipart/form-data; boundary=" << m_boundary << "\r\n";

			for(auto x: m_params)
			{
				data_stream << "--" << m_boundary << "\r\n";
				data_stream << "Content-Disposition: form-data; name=\"" << x.first << "\"\r\n\r\n" << x.second << "\r\n";
			}

			for(auto x: m_files)
			{
				data_stream << "--" << m_boundary << "\r\n";
				data_stream << "Content-Disposition: form-data; name=\"" << x.first << "\"; filename=\"" <<
						x.second.filename << "\"\r\nContent-Transfer-Encoding: binary\r\n\r\n";
				data_stream.write(x.second.filedata.data(), x.second.filedata.size());
				data_stream << "\r\n";
			}
			data_stream << "--" << m_boundary << "--\r\n";
		}
		if(data_stream.tellp() > 0)
		{
			request_stream << "Content-Length: " << data_stream.tellp() << "\r\n";
		}
		request_stream << "Accept: */*\r\n";
		request_stream << "Accept-Encoding: deflate\r\n";
		request_stream << "Connection: close\r\n\r\n";

		if(data_stream.tellp() > 0)
		{
			while(!data_stream.eof())
			{
				char buf[4096]; //Fixme hardcoded
				data_stream.read(buf, 4096);
				request_stream.write(buf, data_stream.gcount());
			}
		}

		boost::asio::async_write(m_socket, *m_request,
				boost::bind(&HttpRequest::handle_write_header, this, boost::asio::placeholders::error));
	}
	else
	{
		m_error(id(), error.message());
	}
}

void HttpRequest::handle_write_header(const boost::system::error_code& error)
{
	boost::asio::async_read_until(m_socket, *m_response, "\r\n\r\n",
			boost::bind(&HttpRequest::handle_headers, this, boost::asio::placeholders::error));

}

void HttpRequest::handle_headers(const boost::system::error_code& error)
{
	if(!error)
	{
		std::istream input(m_response);

		std::string protocol;
		input >> protocol;
		if(!boost::algorithm::starts_with(protocol, "HTTP/"))
		{
			m_error(id(), "Invalid protocol");
			return;
		}

		input >> m_statusCode;

		std::string status_message;
		std::getline(input, status_message);

		std::string header;
		while(std::getline(input, header) && header != "\r")
		{
			auto entry = parseHeaderEntry(header);
			if(boost::iequals(entry.first, "Set-Cookie"))
			{
				parseCookies(entry.second);
			}
			else
			{
				m_headers.insert(entry);
			}
		}

		std::string contentLength = this->header("Content-Length");
		if(!contentLength.empty())
		{
			try
			{
				m_expectedLength = boost::lexical_cast<int>(contentLength);
			}
			catch(boost::bad_lexical_cast& e)
			{
				m_expectedLength = 0;
			}
		}

		if(m_statusCode == 302) // Redirect
		{
			std::string location = this->header("Location");
			if(location.empty())
			{
				m_error(id(), "HttpRequest: redirect to empty location");
				return;
			}
			Url newUrl(location);
			if(newUrl.host().empty())
			{
				newUrl.setHost(m_url.host());
				newUrl.setScheme(m_url.scheme());
			}
			m_method = "GET";
			m_url = newUrl;
			perform();
			return;
		}

		std::string contentEncoding = this->header("Transfer-Encoding");
		if(!contentEncoding.empty())
		{
			if(contentEncoding == "chunked")
			{
				boost::asio::async_read_until(m_socket, *m_response, "\r\n",
						boost::bind(&HttpRequest::handle_chunked, this, boost::asio::placeholders::error));
				return;
			}
		}

		boost::asio::async_read(m_socket, *m_response,
				boost::asio::transfer_at_least(1),
				boost::bind(&HttpRequest::handle_content, this,
						boost::asio::placeholders::error));
	}
	else
	{
		m_error(id(), error.message());
	}
}

void HttpRequest::handle_chunked(const boost::system::error_code& error)
{
	if(!error)
	{
		std::string chunkLength;
		std::istream stream(m_response);
		std::getline(stream, chunkLength);
		m_expectedLength = strtol(chunkLength.c_str(), 0, 16);
		boost::asio::async_read(m_socket, *m_response,
				boost::asio::transfer_at_least(1),
				boost::bind(&HttpRequest::handle_content, this,
						boost::asio::placeholders::error));
	}
	else
	{
		m_error(id(), error.message());
	}
}

void HttpRequest::handle_content(const boost::system::error_code& error)
{
	if(!error)
	{
		// TODO: test performance
		std::istream stream(m_response);
		while(!stream.eof())
		{
			char buf[4096]; //Fixme hardcoded
			stream.read(buf, 4096);
			int tocopy = stream.gcount();
			if((m_readLength + tocopy > m_expectedLength) && m_expectedLength)
				tocopy = m_expectedLength - m_readLength;
			std::copy(buf, buf + tocopy, std::back_inserter(*m_content.get()));
			m_readLength += tocopy;
		}

		m_progress(id(), m_content->size(), m_expectedLength);
		boost::asio::async_read(m_socket, *m_response,
				boost::asio::transfer_at_least(1),
				boost::bind(&HttpRequest::handle_content, this,
						boost::asio::placeholders::error));
	}
	else if(error == boost::asio::error::eof)
	{
		m_finished(id());
	}
	else
	{
		m_error(id(), error.message());
	}
}

std::pair<std::string, std::string> HttpRequest::parseHeaderEntry(const std::string& headerEntry)
{
	size_t delim = headerEntry.find(':');
	if(delim == std::string::npos)
		throw ProtocolException(std::string("Invalid header entry: ") + headerEntry);
	std::string headerName = headerEntry.substr(0, delim);
	trim(headerName);
	std::string headerValue = headerEntry.substr(delim + 1);
	trim(headerValue);
	if(headerName.empty())
		throw ProtocolException(std::string("Invalid header entry: ") + headerEntry);
	to_lower(headerName);
	return std::make_pair(headerName, headerValue);
}

void HttpRequest::parseCookies(const std::string& cookies)
{
	std::vector<std::string> strs;
	boost::split(strs, cookies, boost::is_any_of(";"));
	std::string s = strs[0];
	size_t eqpos = s.find('=');
	if(eqpos == std::string::npos) // TODO signal an error
		return;
	std::string key = s.substr(0, eqpos);
	std::string value = s.substr(eqpos + 1);
	boost::trim(key);
	boost::trim(value);
	m_context->addCookie(m_url.host(), key, value);
}

boost::asio::streambuf* HttpRequest::createRequestStreamBuf()
{
	return new boost::asio::streambuf();
}

boost::asio::streambuf* HttpRequest::createResponseStreamBuf()
{
	return new boost::asio::streambuf();
}

char HttpRequest::randomChar()
{
	unsigned int c = rand() % 64;
	if(c < 10)
		return '0' + c;
	else if(c < 36)
		return 'a' + (c - 10);
	else if(c < 62)
		return 'A' + (c - 36);
	return '0';
}

} /* namespace Network */
} /* namespace Wishmaster2 */
